-- 145_scenarios — Slice D, m/paliad#124 §5 (revised) -- -- Creates paliad.scenarios + paliad.projects.active_scenario_id FK. -- A scenario is a named composition of existing proceedings + flags -- + per-card choices + anchor dates the user can switch between for -- a project (project_id NOT NULL) OR save as an abstract template on -- /tools/verfahrensablauf (project_id IS NULL). -- -- m's 2026-05-26 picks (AskUserQuestion round, doc commit 6e58595): -- Q1: composition shape → primary+spawned (v1); multi-proceeding -- peer compose is the v2 goal. spec.jsonb -- architected for N entries from day 1. -- Q2: scope → per-project + abstract. -- Q3: trigger dates → per-anchor overrides over one base date. -- Q4: storage → NEW paliad.scenarios table with jsonb -- spec (NOT a project_event_choices column -- extension). -- -- "users should not add their own rules" (m, t-paliad-301) — scenarios -- compose existing rules, never author new ones. spec.proceedings[*].code -- must resolve to an existing active paliad.proceeding_types row; -- spec.proceedings[*].anchor_overrides keys must resolve to existing -- submission_codes. Validation happens at the application layer -- (ScenarioService.validateSpec) — not in DB CHECK constraints (too -- expensive to express in pure SQL). -- -- Migration number: 145. Coordination check 2026-05-26 17:38: curie's -- B.2-B.6 migrations land in the 139-143 range. 144 reserved as buffer. -- 145 is the next safe claim. -- -- ADDITIVE ONLY: CREATE TABLE, ALTER ADD COLUMN, indexes, RLS policies. -- Down drops everything. No backfill (zero existing scenarios on day 1). -- -- See docs/design-litigation-planner-2026-05-26.md §5 + §18.4 for the -- design. -- --------------------------------------------------------------- -- 1. The scenarios table -- --------------------------------------------------------------- CREATE TABLE paliad.scenarios ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- project_id NULL = abstract scenario (saved Verfahrensablauf -- template, no Akte). project_id NOT NULL = scenario attached to -- a real Akte. project_id uuid NULL REFERENCES paliad.projects(id) ON DELETE CASCADE, name text NOT NULL, description text NULL, -- spec carries the full composition. Shape documented in the -- design doc §5; the application validates structure before write. spec jsonb NOT NULL, created_by uuid NULL REFERENCES paliad.users(id) ON DELETE SET NULL, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), -- Within a single project, scenario names are unique. Abstract -- scenarios are unique per (created_by, name) so two users can -- each keep a "with_ccr" template without colliding. NULLS NOT -- DISTINCT means a single user can have one "name" per -- (project_id, created_by) tuple, where NULL project_id + -- NULL created_by is a single global namespace (used only by -- seed / system scenarios — none today). CONSTRAINT scenarios_unique_per_scope UNIQUE NULLS NOT DISTINCT (project_id, created_by, name), -- Non-empty name. CONSTRAINT scenarios_name_nonempty CHECK (char_length(name) > 0), -- Non-empty spec — at least an object. The application checks -- structure (version, proceedings[], base_trigger_date format). CONSTRAINT scenarios_spec_object CHECK (jsonb_typeof(spec) = 'object') ); CREATE INDEX scenarios_project_id_idx ON paliad.scenarios(project_id) WHERE project_id IS NOT NULL; CREATE INDEX scenarios_abstract_user_idx ON paliad.scenarios(created_by) WHERE project_id IS NULL; COMMENT ON TABLE paliad.scenarios IS 'Named compositions of existing proceedings + flags + per-card ' 'choices + anchor dates. project_id NULL = abstract template; ' 'project_id NOT NULL = attached to an Akte. Design: ' 'docs/design-litigation-planner-2026-05-26.md §5. (Slice D)'; COMMENT ON COLUMN paliad.scenarios.spec IS 'jsonb composition spec. Shape: {version: int, base_trigger_date: ' 'ISO date, proceedings: [{code, role, flags[], per_card_choices, ' 'anchor_overrides, skip_rules[]}, ...]}. Validated at write-time ' 'by ScenarioService.validateSpec.'; -- --------------------------------------------------------------- -- 2. paliad.projects.active_scenario_id FK -- -- NULL = use today's ad-hoc per-card choice state from -- paliad.project_event_choices (pre-scenario behaviour preserved). -- Non-NULL = the project's current SmartTimeline / Akte-Fristenrechner -- render reads from this scenario's spec instead. -- --------------------------------------------------------------- ALTER TABLE paliad.projects ADD COLUMN active_scenario_id uuid NULL REFERENCES paliad.scenarios(id) ON DELETE SET NULL; COMMENT ON COLUMN paliad.projects.active_scenario_id IS 'FK to paliad.scenarios. NULL = read choices from ' 'paliad.project_event_choices (legacy). Non-NULL = read from the ' 'pointed scenario.spec.'; -- --------------------------------------------------------------- -- 3. RLS — mirror paliad.project_event_choices's pattern (mig 129). -- -- Project-scoped scenarios (project_id NOT NULL) inherit team visibility -- via paliad.can_see_project. Abstract scenarios (project_id IS NULL) -- are private to created_by — only the author can read / write them. -- --------------------------------------------------------------- ALTER TABLE paliad.scenarios ENABLE ROW LEVEL SECURITY; -- Project-scoped: team visibility. DROP POLICY IF EXISTS scenarios_project_select ON paliad.scenarios; CREATE POLICY scenarios_project_select ON paliad.scenarios FOR SELECT USING (project_id IS NOT NULL AND paliad.can_see_project(project_id)); DROP POLICY IF EXISTS scenarios_project_mutate ON paliad.scenarios; CREATE POLICY scenarios_project_mutate ON paliad.scenarios FOR ALL USING (project_id IS NOT NULL AND paliad.can_see_project(project_id)) WITH CHECK (project_id IS NOT NULL AND paliad.can_see_project(project_id)); -- Abstract: owner-only. DROP POLICY IF EXISTS scenarios_abstract_select ON paliad.scenarios; CREATE POLICY scenarios_abstract_select ON paliad.scenarios FOR SELECT USING (project_id IS NULL AND created_by = auth.uid()); DROP POLICY IF EXISTS scenarios_abstract_mutate ON paliad.scenarios; CREATE POLICY scenarios_abstract_mutate ON paliad.scenarios FOR ALL USING (project_id IS NULL AND created_by = auth.uid()) WITH CHECK (project_id IS NULL AND created_by = auth.uid()); -- --------------------------------------------------------------- -- 4. updated_at trigger (mirrors other paliad tables that carry -- updated_at — keep it in lockstep with row mutations). -- --------------------------------------------------------------- CREATE OR REPLACE FUNCTION paliad.scenarios_touch_updated_at() RETURNS trigger AS $$ BEGIN NEW.updated_at = now(); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER scenarios_touch_updated_at_trg BEFORE UPDATE ON paliad.scenarios FOR EACH ROW EXECUTE FUNCTION paliad.scenarios_touch_updated_at(); -- --------------------------------------------------------------- -- 5. Informational NOTICE — schema-only migration, zero rows added. -- --------------------------------------------------------------- DO $$ BEGIN RAISE NOTICE '[mig 145] paliad.scenarios created (0 rows; awaits API usage)'; RAISE NOTICE '[mig 145] paliad.projects.active_scenario_id added (all rows NULL initially)'; END $$;