-- Nullable adoption outputs; declaration inputs and historical rows are untouched. begin; alter table public.profiles add column if not exists active_birth_date date, add column if not exists active_birth_timezone_offset double precision, add column if not exists active_birth_provenance jsonb; create or replace function public.guard_adopted_birth_date() returns trigger language plpgsql set search_path = '' as $$ begin if new.birth_time_status not in ('accepted', 'confirmed') or new.birth_time_status is null or new.active_birth_time is null or (tg_op = 'UPDATE' and ( new.birth_date is distinct from old.birth_date or new.reported_birth_time is distinct from old.reported_birth_time or new.birth_time_source is distinct from old.birth_time_source or new.birth_time_period is distinct from old.birth_time_period or new.declared_window_start is distinct from old.declared_window_start or new.declared_window_end is distinct from old.declared_window_end or new.uncertainty_before_minutes is distinct from old.uncertainty_before_minutes or new.uncertainty_after_minutes is distinct from old.uncertainty_after_minutes or new.latitude is distinct from old.latitude or new.longitude is distinct from old.longitude or new.timezone_id is distinct from old.timezone_id or new.timezone_offset is distinct from old.timezone_offset or (new.active_birth_time is distinct from old.active_birth_time and new.active_birth_provenance is not distinct from old.active_birth_provenance) )) then new.active_birth_date := null; new.active_birth_timezone_offset := null; new.active_birth_provenance := null; end if; return new; end; $$; drop trigger if exists zz_guard_adopted_birth_date on public.profiles; create trigger zz_guard_adopted_birth_date before insert or update on public.profiles for each row execute function public.guard_adopted_birth_date(); -- Internal, service-only validation shared by adoption and confirmation. -- The candidate UUID identifies persisted facts; callers never supply a date. create or replace function public.validate_dated_rectification_candidate( p_case public.agentic_rectification_cases, p_result public.agentic_rectification_results, p_candidate public.agentic_rectification_candidates, p_inference jsonb ) returns date language plpgsql security definer set search_path = '' as $$ declare v_receipt jsonb := p_result.decision_receipt; v_windows jsonb; v_payload jsonb; v_item jsonb; v_interval jsonb; v_window jsonb; v_first timestamp; v_at timestamp; v_start timestamp; v_end timestamp; v_previous timestamp; v_date date; v_expected text; v_count integer; v_union pg_catalog.tsmultirange; v_credible pg_catalog.tsmultirange; v_expected_credible jsonb; v_credible_ranges pg_catalog.tsrange[] := array[]::pg_catalog.tsrange[]; v_top text; begin if v_receipt ->> 'candidate_window_contract' is distinct from 'dated-v1' then -- A partial new contract must never silently become a legacy same-day result. if p_result.algorithm_version = 'rectification-v5-matrix-scoring-9' or v_receipt ? 'candidate_window_contract' or v_receipt ? 'candidate_intervals' or p_candidate.candidate_payload ? 'candidate_date' or coalesce(p_inference ->> 'candidate_set_id', '') like 'dated-v1:%' then raise exception 'agentic_rectification_candidate_state_inconsistent'; end if; return null; end if; v_windows := v_receipt -> 'candidate_intervals'; if jsonb_typeof(v_windows) is distinct from 'array' or jsonb_array_length(v_windows) = 0 or v_windows is distinct from p_case.candidate_range -> 'candidate_intervals' or jsonb_typeof(v_receipt -> 'candidate_timezone_offset') is distinct from 'number' or (v_receipt ->> 'candidate_timezone_offset')::double precision is distinct from (p_case.baseline_birth_snapshot ->> 'timezone_offset')::double precision or coalesce(v_receipt ->> 'candidate_timezone_id', '') is distinct from coalesce(p_case.baseline_birth_snapshot ->> 'timezone_id', '') or jsonb_typeof(p_inference -> 'candidates') is distinct from 'array' or jsonb_typeof(p_inference -> 'credible_intervals') is distinct from 'array' or p_inference ->> 'range_start' is distinct from p_case.candidate_range ->> 'start_time' or p_inference ->> 'range_end' is distinct from p_case.candidate_range ->> 'end_time' then raise exception 'agentic_rectification_candidate_state_inconsistent'; end if; for v_window in select value from jsonb_array_elements(v_windows) loop if coalesce(v_window ->> 'start_at', '') !~ '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$' or coalesce(v_window ->> 'end_at', '') !~ '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$' then raise exception 'agentic_rectification_candidate_state_inconsistent'; end if; v_start := (v_window ->> 'start_at')::timestamp; v_end := (v_window ->> 'end_at')::timestamp; if v_end < v_start or v_start <= v_previous then raise exception 'agentic_rectification_candidate_state_inconsistent'; end if; v_first := coalesce(v_first, v_start); v_previous := v_end; end loop; select count(*), 'dated-v1:' || (p_case.candidate_range ->> 'start_time') || '-' || (p_case.candidate_range ->> 'end_time') || ':' || string_agg( (candidate_payload ->> 'candidate_date') || 'T' || to_char(candidate_time, 'HH24:MI') || '@' || (candidate_payload ->> 'window_index') || '@' || (candidate_payload ->> 'window_offset_minutes') || '@' || (candidate_payload ->> 'segment_index'), ',' order by (candidate_payload ->> 'window_index')::integer) into v_count, v_expected from public.agentic_rectification_candidates where result_id = p_result.id; if v_count = 0 or jsonb_array_length(p_inference -> 'candidates') <> v_count or p_inference ->> 'candidate_set_id' is distinct from v_expected or (select count(distinct value ->> 'window_index') from jsonb_array_elements(p_inference -> 'candidates')) <> v_count then raise exception 'agentic_rectification_candidate_state_inconsistent'; end if; for v_item in select value from jsonb_array_elements(p_inference -> 'candidates') loop select candidate_payload into v_payload from public.agentic_rectification_candidates where result_id = p_result.id and to_char(candidate_time, 'HH24:MI') = v_item ->> 'time'; if not found or coalesce(v_item ->> 'candidate_date', '') !~ '^\d{4}-\d{2}-\d{2}$' or coalesce(v_item ->> 'window_index', '') !~ '^\d+$' or coalesce(v_item ->> 'window_offset_minutes', '') !~ '^\d+$' or coalesce(v_item ->> 'segment_index', '') !~ '^\d+$' or v_item -> 'candidate_date' is distinct from v_payload -> 'candidate_date' or v_item -> 'window_index' is distinct from v_payload -> 'window_index' or v_item -> 'window_offset_minutes' is distinct from v_payload -> 'window_offset_minutes' or v_item -> 'segment_index' is distinct from v_payload -> 'segment_index' or v_item -> 'cluster_intervals' is distinct from v_payload -> 'cluster_intervals' or coalesce(v_item ->> 'status', '') not in ('active', 'equivalent', 'winner', 'eliminated') or jsonb_typeof(v_item -> 'probability') is distinct from 'number' or jsonb_typeof(v_item -> 'posterior_score') is distinct from 'number' or jsonb_typeof(v_item -> 'cluster_intervals') is distinct from 'array' or jsonb_array_length(v_item -> 'cluster_intervals') = 0 then raise exception 'agentic_rectification_candidate_state_inconsistent'; end if; v_at := ((v_item ->> 'candidate_date') || 'T' || (v_item ->> 'time'))::timestamp; v_window := v_windows -> (v_item ->> 'segment_index')::integer; if v_window is null or v_at < (v_window ->> 'start_at')::timestamp or v_at > (v_window ->> 'end_at')::timestamp or extract(epoch from (v_at - v_first)) / 60 <> (v_item ->> 'window_offset_minutes')::integer then raise exception 'agentic_rectification_candidate_state_inconsistent'; end if; if not exists (select 1 from jsonb_array_elements(v_item -> 'cluster_intervals') x where x -> 'segment_index' = v_item -> 'segment_index' and (v_item ->> 'window_index')::integer between (x ->> 'start_index')::integer and (x ->> 'end_index')::integer and v_at between (x ->> 'start_at')::timestamp and (x ->> 'end_at')::timestamp) then raise exception 'agentic_rectification_candidate_state_inconsistent'; end if; for v_interval in select value from jsonb_array_elements(v_item -> 'cluster_intervals') loop v_start := (v_interval ->> 'start_at')::timestamp; v_end := (v_interval ->> 'end_at')::timestamp; v_window := v_windows -> (v_interval ->> 'segment_index')::integer; if v_start is null or v_end is null or v_window is null or v_end < v_start or v_start < (v_window ->> 'start_at')::timestamp or v_end > (v_window ->> 'end_at')::timestamp or (v_interval ->> 'start_offset_minutes')::integer is distinct from (extract(epoch from (v_start - v_first)) / 60)::integer or (v_interval ->> 'end_offset_minutes')::integer is distinct from (extract(epoch from (v_end - v_first)) / 60)::integer or coalesce(v_interval ->> 'start_index', '') !~ '^\d+$' or coalesce(v_interval ->> 'end_index', '') !~ '^\d+$' or (v_interval ->> 'end_index')::integer < (v_interval ->> 'start_index')::integer then raise exception 'agentic_rectification_candidate_state_inconsistent'; end if; end loop; end loop; -- Historical credible semantics envelope active clusters within each declared segment, -- but never bridge separate declared segments. select range_agg(tsrange(first_at, last_at + interval '1 minute', '[)')), jsonb_agg(jsonb_build_object('segment_index', segment_index, 'start_index', first_index, 'end_index', last_index, 'start_offset_minutes', (extract(epoch from (first_at - v_first)) / 60)::integer, 'end_offset_minutes', (extract(epoch from (last_at - v_first)) / 60)::integer, 'start_at', to_char(first_at, 'YYYY-MM-DD"T"HH24:MI'), 'end_at', to_char(last_at, 'YYYY-MM-DD"T"HH24:MI')) order by first_index) into v_union, v_expected_credible from ( select (part ->> 'segment_index')::integer as segment_index, min((part ->> 'start_index')::integer) as first_index, max((part ->> 'end_index')::integer) as last_index, min((part ->> 'start_at')::timestamp) as first_at, max((part ->> 'end_at')::timestamp) as last_at from jsonb_array_elements(p_inference -> 'candidates') candidate, lateral jsonb_array_elements(candidate -> 'cluster_intervals') part where candidate ->> 'status' <> 'eliminated' and (select (value ->> 'posterior_score')::numeric from jsonb_array_elements(p_inference -> 'candidates') where value ->> 'status' <> 'eliminated' order by (value ->> 'probability')::numeric desc, (value ->> 'posterior_score')::numeric desc, (value ->> 'window_index')::integer limit 1) - (candidate ->> 'posterior_score')::numeric < 8 group by (part ->> 'segment_index')::integer ) segments; for v_interval in select value from jsonb_array_elements(p_inference -> 'credible_intervals') loop v_start := (v_interval ->> 'start_at')::timestamp; v_end := (v_interval ->> 'end_at')::timestamp; if v_start is null or v_end is null or v_end < v_start then raise exception 'agentic_rectification_candidate_state_inconsistent'; end if; v_credible_ranges := array_append(v_credible_ranges, tsrange(v_start, v_end + interval '1 minute', '[)')); end loop; select range_agg(x) into v_credible from unnest(v_credible_ranges) x; select value ->> 'time' into v_top from jsonb_array_elements(p_inference -> 'candidates') where value ->> 'status' <> 'eliminated' order by (value ->> 'probability')::numeric desc, (value ->> 'posterior_score')::numeric desc, (value ->> 'window_index')::integer limit 1; if v_union is null or v_credible is distinct from v_union or p_inference -> 'credible_intervals' is distinct from v_expected_credible or p_inference ->> 'representative_time' is distinct from v_top or not exists (select 1 from jsonb_array_elements(p_inference -> 'candidates') where value ->> 'time' = to_char(p_candidate.candidate_time, 'HH24:MI') and value ->> 'status' <> 'eliminated') then raise exception 'agentic_rectification_candidate_state_inconsistent'; end if; v_date := (p_candidate.candidate_payload ->> 'candidate_date')::date; if v_date is null then raise exception 'agentic_rectification_candidate_state_inconsistent'; end if; return v_date; end; $$; revoke all on function public.validate_dated_rectification_candidate(public.agentic_rectification_cases, public.agentic_rectification_results, public.agentic_rectification_candidates, jsonb) from public, anon, authenticated, service_role; 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, p_credible_range jsonb default null ) 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; v_adopted jsonb; v_adopted_start text; v_adopted_end text; v_adopted_rep text; v_adopted_width integer; v_saved_date date; v_provenance jsonb; v_saved_offset double precision; 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; v_saved_date := public.validate_dated_rectification_candidate(v_case, v_result, v_candidate, v_inference); if v_saved_date is null then 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; else v_top_active_time := v_inference ->> 'representative_time'; v_saved_offset := (v_result.decision_receipt ->> 'candidate_timezone_offset')::double precision; v_provenance := jsonb_build_object('contract', 'dated-v1', 'case_id', p_case_id, 'result_id', p_result_id, 'candidate_id', p_candidate_id, 'declared_birth_date', v_profile.birth_date, 'candidate_date', v_saved_date, 'candidate_time', to_char(v_candidate.candidate_time, 'HH24:MI'), 'timezone_offset', v_saved_offset, 'timezone_id', v_result.decision_receipt ->> 'candidate_timezone_id', 'source', 'persisted_candidate_calculation', 'adopted_at', pg_catalog.now()); 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' and v_profile.active_birth_date is not distinct from v_saved_date then v_response := jsonb_build_object( 'success', true, 'saved_time', pg_catalog.to_char(v_candidate.candidate_time, 'HH24:MI'), 'saved_date', coalesce(v_saved_date, v_profile.birth_date), 'day_offset', coalesce(v_saved_date - v_profile.birth_date, 0), 'date_changed', coalesce(v_saved_date <> v_profile.birth_date, false), 'saved_timezone_offset', coalesce(v_saved_offset, v_profile.timezone_offset), '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, active_birth_date = v_saved_date, active_birth_timezone_offset = v_saved_offset, active_birth_provenance = v_provenance, 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; v_adopted := null; if p_credible_range is not null and jsonb_typeof(p_credible_range) = 'object' then v_adopted_start := nullif(btrim(coalesce(p_credible_range->>'start_time', '')), ''); v_adopted_end := nullif(btrim(coalesce(p_credible_range->>'end_time', '')), ''); v_adopted_rep := nullif(btrim(coalesce(p_credible_range->>'representative_time', '')), ''); begin v_adopted_width := (p_credible_range->>'width_minutes')::integer; exception when others then v_adopted_width := null; end; end if; if v_adopted_start is null or v_adopted_end is null or v_adopted_start !~ '^([01][0-9]|2[0-3]):[0-5][0-9]$' or v_adopted_end !~ '^([01][0-9]|2[0-3]):[0-5][0-9]$' then v_adopted_start := v_inference -> 'credible_range' ->> 0; v_adopted_end := v_inference -> 'credible_range' ->> 1; v_adopted_rep := v_inference ->> 'representative_time'; v_adopted_width := null; end if; if v_adopted_start is not null and v_adopted_end is not null and v_adopted_start ~ '^([01][0-9]|2[0-3]):[0-5][0-9]$' and v_adopted_end ~ '^([01][0-9]|2[0-3]):[0-5][0-9]$' then v_adopted := jsonb_strip_nulls(jsonb_build_object( 'start_time', v_adopted_start, 'end_time', v_adopted_end, 'representative_time', coalesce(v_adopted_rep, v_top_active_time), 'width_minutes', v_adopted_width, 'source', 'inference_credible_range' )); end if; if v_saved_date is not null then v_adopted := jsonb_build_object( 'candidate_window_contract', 'dated-v1', 'intervals', v_inference -> 'credible_intervals', 'representative_time', to_char(v_candidate.candidate_time, 'HH24:MI'), 'representative_date', v_saved_date, 'source', 'inference_credible_range'); if jsonb_array_length(v_inference -> 'credible_intervals') = 1 then v_adopted := v_adopted || jsonb_build_object( 'start_time', substring(v_inference -> 'credible_intervals' -> 0 ->> 'start_at' from 12 for 5), 'end_time', substring(v_inference -> 'credible_intervals' -> 0 ->> 'end_at' from 12 for 5)); end if; end if; update public.agentic_rectification_cases set status = 'candidate_accepted', accepted_time = v_candidate.candidate_time, confirmed_time = null, completed_at = null, adopted_credible_range = coalesce(v_adopted, adopted_credible_range), 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'), 'saved_date', coalesce(v_saved_date, v_profile.birth_date), 'day_offset', coalesce(v_saved_date - v_profile.birth_date, 0), 'date_changed', coalesce(v_saved_date <> v_profile.birth_date, false), 'saved_timezone_offset', coalesce(v_saved_offset, v_profile.timezone_offset), '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, jsonb) from public, anon, authenticated; grant execute on function public.accept_agentic_rectification_candidate_for_case_v2(uuid, uuid, uuid, uuid, uuid, jsonb) to service_role; create or replace function public.confirm_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, p_consent_quote text, p_source_turn_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_turn public.agentic_rectification_turns%rowtype; v_snapshot jsonb; v_existing_decision public.agentic_rectification_candidate_decisions%rowtype; v_response jsonb; v_saved_date date; v_saved_offset double precision; v_provenance jsonb; v_inference 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 or p_source_turn_id is null or length(btrim(coalesce(p_consent_quote, ''))) = 0 then raise exception 'agentic_rectification_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 <> 'confirm' 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; select inference_state into v_inference from public.agentic_rectification_inference_transitions where case_id = p_case_id and result_id = p_result_id order by revision desc limit 1; v_inference := coalesce(v_inference, v_result.decision_receipt -> 'inference_state'); v_saved_date := public.validate_dated_rectification_candidate(v_case, v_result, v_candidate, v_inference); if v_saved_date is not null then v_saved_offset := (v_result.decision_receipt ->> 'candidate_timezone_offset')::double precision; v_provenance := jsonb_build_object('contract', 'dated-v1', 'case_id', p_case_id, 'result_id', p_result_id, 'candidate_id', p_candidate_id, 'declared_birth_date', v_case.baseline_birth_snapshot ->> 'birth_date', 'candidate_date', v_saved_date, 'candidate_time', to_char(v_candidate.candidate_time, 'HH24:MI'), 'timezone_offset', v_saved_offset, 'timezone_id', v_result.decision_receipt ->> 'candidate_timezone_id', 'source', 'persisted_candidate_calculation', 'adopted_at', pg_catalog.now()); end if; if v_case.status = 'confirmed' then if v_result.selected_candidate_id is distinct from p_candidate_id or v_case.confirmed_time is distinct from v_candidate.candidate_time then raise exception 'agentic_rectification_case_already_confirmed' using errcode = 'P0001'; end if; v_response := jsonb_build_object( 'success', true, 'saved_time', pg_catalog.to_char(v_candidate.candidate_time, 'HH24:MI'), 'saved_date', coalesce(v_saved_date, (v_case.baseline_birth_snapshot ->> 'birth_date')::date), 'day_offset', coalesce(v_saved_date - (v_case.baseline_birth_snapshot ->> 'birth_date')::date, 0), 'date_changed', coalesce(v_saved_date <> (v_case.baseline_birth_snapshot ->> 'birth_date')::date, false), 'saved_timezone_offset', coalesce(v_saved_offset, (v_case.baseline_birth_snapshot ->> 'timezone_offset')::double precision), 'status', 'confirmed', 'result_id', v_result.id, 'candidate_id', v_candidate.id, 'case_status', 'confirmed', '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, 'confirm', v_response ); return v_response; end if; if v_case.status in ('closed', 'abandoned', 'superseded') then raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001'; end if; select * into v_turn from public.agentic_rectification_turns where id = p_source_turn_id and case_id = p_case_id; if not found then raise exception 'agentic_rectification_turn_not_found' using errcode = 'P0001'; end if; if v_turn.user_message is null or position( public.agentic_rectification_normalize_quote(p_consent_quote) in public.agentic_rectification_normalize_quote(v_turn.user_message) ) = 0 then raise exception 'agentic_rectification_consent_not_grounded' 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.confirmation_allowed then raise exception 'agentic_rectification_confirmation_blocked' using errcode = 'P0001'; end if; if not v_candidate.is_representative or v_result.representative_time is distinct from v_candidate.candidate_time then raise exception 'agentic_rectification_confirmation_exact_gate_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; if v_result.selected_candidate_id is null then if v_result.selected_time is not null then raise exception 'agentic_rectification_candidate_selection_blocked' 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.active_birth_time is distinct from (v_snapshot ->> 'active_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.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_offset is distinct from (v_snapshot ->> 'timezone_offset')::double precision then raise exception 'agentic_rectification_candidate_profile_changed' using errcode = 'P0001'; end if; elsif v_result.selected_candidate_id <> p_candidate_id or v_result.selected_time is distinct from v_candidate.candidate_time or v_result.selection_kind is distinct from 'user_accepted' or v_case.status is distinct from 'candidate_accepted' or v_case.accepted_time is distinct from v_candidate.candidate_time or v_profile.active_birth_time is distinct from v_candidate.candidate_time or v_profile.birth_time_status is distinct from 'accepted' then raise exception 'agentic_rectification_candidate_selection_blocked' using errcode = 'P0001'; end if; update public.profiles set active_birth_time = v_candidate.candidate_time, active_birth_date = v_saved_date, active_birth_timezone_offset = v_saved_offset, active_birth_provenance = v_provenance, birth_time = v_candidate.candidate_time, birth_time_status = 'confirmed', 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 = 'engine_confirmed', 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 = 'confirmed', accepted_time = v_candidate.candidate_time, confirmed_time = v_candidate.candidate_time, completed_at = pg_catalog.now(), 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'), 'saved_date', coalesce(v_saved_date, (v_case.baseline_birth_snapshot ->> 'birth_date')::date), 'day_offset', coalesce(v_saved_date - (v_case.baseline_birth_snapshot ->> 'birth_date')::date, 0), 'date_changed', coalesce(v_saved_date <> (v_case.baseline_birth_snapshot ->> 'birth_date')::date, false), 'saved_timezone_offset', coalesce(v_saved_offset, (v_case.baseline_birth_snapshot ->> 'timezone_offset')::double precision), 'status', 'confirmed', 'result_id', v_result.id, 'candidate_id', v_candidate.id, 'case_status', 'confirmed', '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, 'confirm', v_response ); return v_response; end; $$; revoke all on function public.confirm_agentic_rectification_candidate_for_case_v2(uuid, uuid, uuid, uuid, uuid, text, uuid) from public, anon, authenticated; grant execute on function public.confirm_agentic_rectification_candidate_for_case_v2(uuid, uuid, uuid, uuid, uuid, text, uuid) to service_role; create or replace function public.read_report_candidate_range( p_user_id uuid, p_rectification_case_id uuid default null ) returns jsonb language plpgsql stable security definer set search_path = '' as $$ declare v_start text; v_end text; v_range jsonb; begin if p_user_id is null then return null; end if; if p_rectification_case_id is not null then select to_char(c.candidate_start, 'HH24:MI'), to_char(c.candidate_end, 'HH24:MI') into v_start, v_end from public.birth_time_rectification_cases as c where c.id = p_rectification_case_id and c.user_id = p_user_id and c.status in ('confirmed', 'completed') and c.candidate_start is not null and c.candidate_end is not null; if found then return jsonb_build_object('start_time', v_start, 'end_time', v_end); end if; end if; select case when c.adopted_credible_range ->> 'candidate_window_contract' = 'dated-v1' then c.adopted_credible_range when c.adopted_credible_range is not null and nullif(btrim(coalesce(c.adopted_credible_range->>'start_time', '')), '') is not null and nullif(btrim(coalesce(c.adopted_credible_range->>'end_time', '')), '') is not null then c.adopted_credible_range else c.candidate_range end into v_range from public.agentic_rectification_cases as c where c.user_id = p_user_id and c.status = 'candidate_accepted' order by c.updated_at desc limit 1; if v_range is null then return null; end if; if v_range ->> 'candidate_window_contract' = 'dated-v1' then return jsonb_build_object('candidate_intervals', v_range -> 'intervals'); end if; v_start := nullif(btrim(coalesce(v_range->>'start_time', '')), ''); v_end := nullif(btrim(coalesce(v_range->>'end_time', '')), ''); if v_start is null or v_end is null then return null; end if; return jsonb_build_object('start_time', v_start, 'end_time', v_end); end; $$; revoke all on function public.read_report_candidate_range(uuid, uuid) from public, anon, authenticated; grant execute on function public.read_report_candidate_range(uuid, uuid) to service_role; commit;