begin; -- Read projection and acceptance must agree on the same authoritative transition. -- A malformed or internally contradictory ledger row cannot replace the immutable receipt. create or replace function public.compose_agentic_rectification_decision_receipt( p_case_id uuid, p_result_id uuid, p_receipt jsonb ) returns jsonb language plpgsql stable security definer set search_path = '' as $$ declare v_transition public.agentic_rectification_inference_transitions%rowtype; v_receipt jsonb; begin v_receipt := case when p_receipt is not null and jsonb_typeof(p_receipt) = 'object' then p_receipt else '{}'::jsonb end; if p_case_id is null or p_result_id is null then return v_receipt; end if; select * into v_transition from public.agentic_rectification_inference_transitions where case_id = p_case_id and result_id = p_result_id order by revision desc limit 1; if not found then return v_receipt; end if; if jsonb_typeof(v_transition.inference_state) <> 'object' or jsonb_typeof(v_transition.inference_state -> 'revision') is distinct from 'number' or coalesce((v_transition.inference_state ->> 'revision') !~ '^[0-9]+$', true) or jsonb_typeof(v_transition.inference_state -> 'candidate_set_id') is distinct from 'string' then return v_receipt - 'inference_state' - 'decision_state_fingerprint'; end if; if (v_transition.inference_state ->> 'revision')::integer is distinct from v_transition.revision or v_transition.inference_state ->> 'candidate_set_id' is distinct from v_transition.candidate_set_id then return v_receipt - 'inference_state' - 'decision_state_fingerprint'; end if; return jsonb_set( jsonb_set(v_receipt, '{inference_state}', v_transition.inference_state, true), '{decision_state_fingerprint}', to_jsonb(v_transition.decision_state_fingerprint), true ); end; $$; revoke all on function public.compose_agentic_rectification_decision_receipt(uuid, uuid, jsonb) from public, anon, authenticated; grant execute on function public.compose_agentic_rectification_decision_receipt(uuid, uuid, jsonb) to service_role; -- A fresh rectification Case intentionally snapshots active_birth_time as null so -- an older accepted chart minute cannot become the next Case's calculation input. -- Compare only the inputs that shaped the live result, on both first acceptance -- and reselection; active_birth_time is the output of acceptance, not an input. create or replace function public.accept_agentic_rectification_candidate_for_case_v2( p_user_id uuid, p_case_id uuid, p_result_id uuid, p_candidate_id uuid, p_request_id uuid ) returns jsonb language plpgsql security definer set search_path = '' as $$ declare v_case public.agentic_rectification_cases%rowtype; v_result public.agentic_rectification_results%rowtype; v_candidate public.agentic_rectification_candidates%rowtype; v_profile public.profiles%rowtype; v_snapshot jsonb; v_inference jsonb; v_expected_candidate_set_id text; v_persisted_candidate_count integer; v_top_active_time text; v_active_range_start text; v_active_range_end text; v_latest_transition public.agentic_rectification_inference_transitions%rowtype; v_existing_decision public.agentic_rectification_candidate_decisions%rowtype; v_response jsonb; begin if p_user_id is null or p_case_id is null or p_result_id is null or p_candidate_id is null or p_request_id is null then raise exception 'agentic_rectification_candidate_invalid_input' using errcode = 'P0001'; end if; perform pg_catalog.pg_advisory_xact_lock( pg_catalog.hashtextextended(p_user_id::text || ':' || p_request_id::text, 0) ); select * into v_case from public.agentic_rectification_cases where id = p_case_id and user_id = p_user_id for update; if not found then raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001'; end if; select * into v_result from public.agentic_rectification_results where id = p_result_id and user_id = p_user_id and case_id = p_case_id for update; if not found then raise exception 'agentic_rectification_candidate_not_found' using errcode = 'P0001'; end if; select * into v_candidate from public.agentic_rectification_candidates where id = p_candidate_id and result_id = p_result_id and user_id = p_user_id and case_id = p_case_id; if not found then raise exception 'agentic_rectification_candidate_not_found' using errcode = 'P0001'; end if; select * into v_existing_decision from public.agentic_rectification_candidate_decisions where user_id = p_user_id and request_id = p_request_id for update; if found then if v_existing_decision.decision_kind <> 'accept' or v_existing_decision.case_id <> p_case_id or v_existing_decision.result_id <> p_result_id or v_existing_decision.candidate_id <> p_candidate_id then raise exception 'agentic_rectification_candidate_request_conflict' using errcode = 'P0001'; end if; return jsonb_set(v_existing_decision.response, '{idempotent}', 'true'::jsonb, true); end if; if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001'; end if; if v_result.invalidated_at is not null or v_result.expires_at <= pg_catalog.now() then raise exception 'agentic_rectification_candidate_expired' using errcode = 'P0001'; end if; if not v_result.display_allowed or not v_result.selection_allowed then raise exception 'agentic_rectification_candidate_selection_blocked' using errcode = 'P0001'; end if; if exists ( select 1 from public.agentic_rectification_results newer where newer.user_id = p_user_id and newer.case_id = p_case_id and newer.invalidated_at is null and newer.created_at > v_result.created_at ) then raise exception 'agentic_rectification_candidate_superseded' using errcode = 'P0001'; end if; select * into v_profile from public.profiles where id = p_user_id for update; if not found then raise exception 'agentic_rectification_candidate_profile_changed' using errcode = 'P0001'; end if; v_snapshot := v_case.baseline_birth_snapshot; if v_profile.birth_date is distinct from (v_snapshot ->> 'birth_date')::date or v_profile.reported_birth_time is distinct from (v_snapshot ->> 'reported_birth_time')::time without time zone or v_profile.birth_time_source is distinct from v_snapshot ->> 'birth_time_source' or v_profile.birth_time_period is distinct from v_snapshot ->> 'birth_time_period' or v_profile.declared_window_start is distinct from v_snapshot ->> 'declared_window_start' or v_profile.declared_window_end is distinct from v_snapshot ->> 'declared_window_end' or v_profile.uncertainty_before_minutes is distinct from (v_snapshot ->> 'uncertainty_before_minutes')::integer or v_profile.uncertainty_after_minutes is distinct from (v_snapshot ->> 'uncertainty_after_minutes')::integer or v_profile.latitude is distinct from (v_snapshot ->> 'latitude')::double precision or v_profile.longitude is distinct from (v_snapshot ->> 'longitude')::double precision or v_profile.timezone_id is distinct from v_snapshot ->> 'timezone_id' or v_profile.timezone_offset is distinct from (v_snapshot ->> 'timezone_offset')::double precision then raise exception 'agentic_rectification_candidate_profile_changed' using errcode = 'P0001'; end if; -- Adoption is a trust-boundary write. Structured answers append inference -- transitions instead of mutating the engine result, so the latest transition -- is authoritative when present; persisted display candidates remain a cache. select * into v_latest_transition from public.agentic_rectification_inference_transitions where case_id = p_case_id and result_id = v_result.id order by revision desc limit 1; v_inference := case when v_latest_transition.id is not null then v_latest_transition.inference_state else v_result.decision_receipt -> 'inference_state' end; if v_inference is null or jsonb_typeof(v_inference) <> 'object' or jsonb_typeof(v_inference -> 'candidates') <> 'array' or jsonb_array_length(v_inference -> 'candidates') = 0 or jsonb_typeof(v_inference -> 'revision') is distinct from 'number' or coalesce((v_inference ->> 'revision') !~ '^[0-9]+$', true) or length(btrim(coalesce(v_inference ->> 'candidate_set_id', ''))) = 0 or (v_inference ->> 'range_start') is distinct from v_case.candidate_range ->> 'start_time' or (v_inference ->> 'range_end') is distinct from v_case.candidate_range ->> 'end_time' then raise exception 'agentic_rectification_candidate_state_inconsistent' using errcode = 'P0001'; end if; if v_latest_transition.id is not null and ( v_latest_transition.revision is distinct from (v_inference ->> 'revision')::integer or v_latest_transition.candidate_set_id is distinct from v_inference ->> 'candidate_set_id' ) then raise exception 'agentic_rectification_candidate_state_inconsistent' using errcode = 'P0001'; end if; select count(*), (v_case.candidate_range ->> 'start_time') || '-' || (v_case.candidate_range ->> 'end_time') || ':' || string_agg(pg_catalog.to_char(candidate_time, 'HH24:MI'), ',' order by candidate_time) into v_persisted_candidate_count, v_expected_candidate_set_id from public.agentic_rectification_candidates where result_id = v_result.id and user_id = p_user_id and case_id = p_case_id; if v_persisted_candidate_count = 0 or jsonb_array_length(v_inference -> 'candidates') <> v_persisted_candidate_count or (v_inference ->> 'candidate_set_id') is distinct from v_expected_candidate_set_id or exists ( select 1 from pg_catalog.jsonb_array_elements(v_inference -> 'candidates') as item(value) where jsonb_typeof(item.value) <> 'object' ) then raise exception 'agentic_rectification_candidate_state_inconsistent' using errcode = 'P0001'; end if; if exists ( select 1 from pg_catalog.jsonb_array_elements(v_inference -> 'candidates') as item(value) where coalesce((item.value ->> 'time') !~ '^([01][0-9]|2[0-3]):[0-5][0-9]$', true) or coalesce(item.value ->> 'status', '') not in ('active', 'equivalent', 'winner', 'eliminated') or jsonb_typeof(item.value -> 'probability') is distinct from 'number' or jsonb_typeof(item.value -> 'posterior_score') is distinct from 'number' or jsonb_typeof(item.value -> 'cluster_range') is distinct from 'array' ) then raise exception 'agentic_rectification_candidate_state_inconsistent' using errcode = 'P0001'; end if; if exists ( select 1 from pg_catalog.jsonb_array_elements(v_inference -> 'candidates') as item(value) where jsonb_array_length(item.value -> 'cluster_range') is distinct from 2 or coalesce((item.value -> 'cluster_range' ->> 0) !~ '^([01][0-9]|2[0-3]):[0-5][0-9]$', true) or coalesce((item.value -> 'cluster_range' ->> 1) !~ '^([01][0-9]|2[0-3]):[0-5][0-9]$', true) or item.value -> 'cluster_range' ->> 0 > item.value ->> 'time' or item.value -> 'cluster_range' ->> 1 < item.value ->> 'time' or not exists ( select 1 from public.agentic_rectification_candidates persisted where persisted.result_id = v_result.id and pg_catalog.to_char(persisted.candidate_time, 'HH24:MI') = item.value ->> 'time' ) ) or ( select count(distinct item.value ->> 'time') from pg_catalog.jsonb_array_elements(v_inference -> 'candidates') as item(value) ) <> v_persisted_candidate_count then raise exception 'agentic_rectification_candidate_state_inconsistent' using errcode = 'P0001'; end if; select item.value ->> 'time' into v_top_active_time from pg_catalog.jsonb_array_elements(v_inference -> 'candidates') as item(value) where item.value ->> 'status' <> 'eliminated' order by (item.value ->> 'probability')::numeric desc, (item.value ->> 'posterior_score')::numeric desc, item.value ->> 'time' limit 1; select min(item.value -> 'cluster_range' ->> 0), max(item.value -> 'cluster_range' ->> 1) into v_active_range_start, v_active_range_end from pg_catalog.jsonb_array_elements(v_inference -> 'candidates') as item(value) where item.value ->> 'status' <> 'eliminated'; if v_top_active_time is null or (v_inference ->> 'representative_time') is distinct from v_top_active_time or jsonb_typeof(v_inference -> 'credible_range') <> 'array' or jsonb_array_length(v_inference -> 'credible_range') <> 2 or (v_inference -> 'credible_range' ->> 0) is distinct from v_active_range_start or (v_inference -> 'credible_range' ->> 1) is distinct from v_active_range_end or not exists ( select 1 from pg_catalog.jsonb_array_elements(v_inference -> 'candidates') as item(value) where item.value ->> 'time' = pg_catalog.to_char(v_candidate.candidate_time, 'HH24:MI') and item.value ->> 'status' <> 'eliminated' ) then raise exception 'agentic_rectification_candidate_state_inconsistent' using errcode = 'P0001'; end if; if v_result.selected_candidate_id is not null then if v_result.selected_candidate_id = p_candidate_id and v_result.selected_time is not distinct from v_candidate.candidate_time and v_case.accepted_time is not distinct from v_candidate.candidate_time and v_result.selection_kind is not distinct from 'user_accepted' and v_profile.active_birth_time is not distinct from v_candidate.candidate_time and v_profile.birth_time_status is not distinct from 'accepted' then v_response := jsonb_build_object( 'success', true, 'saved_time', pg_catalog.to_char(v_candidate.candidate_time, 'HH24:MI'), 'status', 'accepted', 'result_id', v_result.id, 'candidate_id', v_candidate.id, 'case_status', 'candidate_accepted', 'idempotent', true ); insert into public.agentic_rectification_candidate_decisions ( user_id, case_id, result_id, candidate_id, request_id, decision_kind, response ) values ( p_user_id, p_case_id, p_result_id, p_candidate_id, p_request_id, 'accept', v_response ); return v_response; end if; if v_case.status is distinct from 'candidate_accepted' or v_result.selection_kind is distinct from 'user_accepted' or v_profile.active_birth_time is distinct from v_result.selected_time or v_profile.birth_time_status is distinct from 'accepted' then raise exception 'agentic_rectification_candidate_selection_blocked' using errcode = 'P0001'; end if; else if v_result.selected_time is not null then raise exception 'agentic_rectification_candidate_selection_blocked' using errcode = 'P0001'; end if; end if; update public.profiles set active_birth_time = v_candidate.candidate_time, birth_time = v_candidate.candidate_time, birth_time_status = 'accepted', rectification_confidence = case when v_result.overall_confidence = 'high' then 100 when v_result.overall_confidence = 'medium' then 70 else 40 end, updated_at = pg_catalog.now() where id = p_user_id; update public.agentic_rectification_results set selected_candidate_id = p_candidate_id, selected_time = v_candidate.candidate_time, selection_kind = 'user_accepted', selected_at = pg_catalog.now(), updated_at = pg_catalog.now() where id = v_result.id; update public.agentic_rectification_results set invalidated_at = pg_catalog.now(), updated_at = pg_catalog.now() where user_id = p_user_id and case_id = p_case_id and id <> v_result.id and invalidated_at is null and selected_time is null; update public.agentic_rectification_cases set status = 'candidate_accepted', accepted_time = v_candidate.candidate_time, confirmed_time = null, completed_at = null, last_activity_at = pg_catalog.now(), updated_at = pg_catalog.now() where id = v_case.id; v_response := jsonb_build_object( 'success', true, 'saved_time', pg_catalog.to_char(v_candidate.candidate_time, 'HH24:MI'), 'status', 'accepted', 'result_id', v_result.id, 'candidate_id', v_candidate.id, 'case_status', 'candidate_accepted', 'idempotent', false ); insert into public.agentic_rectification_candidate_decisions ( user_id, case_id, result_id, candidate_id, request_id, decision_kind, response ) values ( p_user_id, p_case_id, p_result_id, p_candidate_id, p_request_id, 'accept', v_response ); return v_response; end; $$; revoke all on function public.accept_agentic_rectification_candidate_for_case_v2(uuid, uuid, uuid, uuid, uuid) from public, anon, authenticated; grant execute on function public.accept_agentic_rectification_candidate_for_case_v2(uuid, uuid, uuid, uuid, uuid) to service_role; -- Keep candidate freshness tied to calculation inputs only. Display labels and -- accepted/active time are outputs or presentation state; declared windows are -- calculation inputs and must invalidate live candidates immediately. create or replace function public.agentic_rectification_profiles_rebaseline_guard() returns trigger language plpgsql security definer set search_path = '' as $$ begin if old.birth_date is distinct from new.birth_date or old.reported_birth_time is distinct from new.reported_birth_time or old.birth_time_source is distinct from new.birth_time_source or old.birth_time_period is distinct from new.birth_time_period or old.declared_window_start is distinct from new.declared_window_start or old.declared_window_end is distinct from new.declared_window_end or old.uncertainty_before_minutes is distinct from new.uncertainty_before_minutes or old.uncertainty_after_minutes is distinct from new.uncertainty_after_minutes or old.latitude is distinct from new.latitude or old.longitude is distinct from new.longitude or old.timezone_id is distinct from new.timezone_id or old.timezone_offset is distinct from new.timezone_offset then update public.agentic_rectification_cases set status = 'needs_rebaseline', updated_at = pg_catalog.now(), last_activity_at = pg_catalog.now() where user_id = new.id and status = any (public.agentic_rectification_resumable_statuses()); end if; return new; end; $$; revoke all on function public.agentic_rectification_profiles_rebaseline_guard() from public, anon, authenticated; drop trigger if exists agentic_rectification_profiles_rebaseline_guard_trigger on public.profiles; create trigger agentic_rectification_profiles_rebaseline_guard_trigger after update of birth_date, reported_birth_time, birth_time_source, birth_time_period, declared_window_start, declared_window_end, uncertainty_before_minutes, uncertainty_after_minutes, latitude, longitude, timezone_id, timezone_offset on public.profiles for each row execute function public.agentic_rectification_profiles_rebaseline_guard(); create or replace function public.invalidate_agentic_rectification_results_on_profile_change() returns trigger language plpgsql security definer set search_path = '' as $$ begin if old.birth_date is distinct from new.birth_date or old.reported_birth_time is distinct from new.reported_birth_time or old.birth_time_source is distinct from new.birth_time_source or old.birth_time_period is distinct from new.birth_time_period or old.declared_window_start is distinct from new.declared_window_start or old.declared_window_end is distinct from new.declared_window_end or old.uncertainty_before_minutes is distinct from new.uncertainty_before_minutes or old.uncertainty_after_minutes is distinct from new.uncertainty_after_minutes or old.latitude is distinct from new.latitude or old.longitude is distinct from new.longitude or old.timezone_id is distinct from new.timezone_id or old.timezone_offset is distinct from new.timezone_offset then update public.agentic_rectification_results set invalidated_at = pg_catalog.now(), updated_at = pg_catalog.now() where user_id = new.id and invalidated_at is null; end if; return new; end; $$; revoke all on function public.invalidate_agentic_rectification_results_on_profile_change() from public, anon, authenticated; drop trigger if exists profiles_invalidate_agentic_rectification_results on public.profiles; create trigger profiles_invalidate_agentic_rectification_results after update of birth_date, reported_birth_time, birth_time_source, birth_time_period, declared_window_start, declared_window_end, uncertainty_before_minutes, uncertainty_after_minutes, latitude, longitude, timezone_id, timezone_offset on public.profiles for each row execute function public.invalidate_agentic_rectification_results_on_profile_change(); commit;