-- Persist Agentic Rectification candidates and atomically apply a user-selected candidate. -- A user selection is usable chart time (`accepted`) without claiming the engine uniquely confirmed it. begin; alter table public.profiles drop constraint if exists profiles_birth_time_status_check; alter table public.profiles add constraint profiles_birth_time_status_check check ( birth_time_status is null or birth_time_status in ( 'reported', 'assessing', 'rectifying', 'candidate', 'accepted', 'confirmed' ) ); create table public.agentic_rectification_results ( id uuid primary key default gen_random_uuid(), user_id uuid not null references auth.users(id) on delete cascade, session_id uuid not null references public.chat_sessions(id) on delete cascade, engine_result_id text not null, canonical_input_hash text not null, algorithm_version text not null, candidate_range jsonb not null, candidates jsonb not null check (jsonb_typeof(candidates) = 'array'), overall_confidence text not null check (overall_confidence in ('low', 'medium', 'high')), margin_percent numeric, selection_allowed boolean not null default false, confirmation_allowed boolean not null default false, representative_time time without time zone, baseline_birth_date date not null, baseline_reported_birth_time time without time zone, baseline_active_birth_time time without time zone, baseline_birth_time_source text, baseline_birth_time_period text, baseline_uncertainty_before_minutes integer, baseline_uncertainty_after_minutes integer, baseline_latitude double precision not null, baseline_longitude double precision not null, baseline_timezone_offset double precision not null, selected_time time without time zone, selection_kind text check (selection_kind in ('user_accepted', 'engine_confirmed')), selected_at timestamptz, invalidated_at timestamptz, expires_at timestamptz not null default (now() + interval '30 days'), created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (user_id, session_id, engine_result_id) ); create index agentic_rectification_results_latest_idx on public.agentic_rectification_results (user_id, session_id, created_at desc) where invalidated_at is null; alter table public.agentic_rectification_results enable row level security; create policy agentic_rectification_results_select_own on public.agentic_rectification_results for select to authenticated using ((select auth.uid()) = user_id); revoke all on table public.agentic_rectification_results from public, anon, authenticated, service_role; grant select on table public.agentic_rectification_results to authenticated; grant all on table public.agentic_rectification_results to service_role; 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.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_offset is distinct from new.timezone_offset or ( (old.active_birth_time is distinct from new.active_birth_time or old.birth_time_status is distinct from new.birth_time_status) and coalesce(new.birth_time_status, '') not in ('accepted', 'confirmed') ) 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, active_birth_time, birth_time_status, birth_time_source, birth_time_period, uncertainty_before_minutes, uncertainty_after_minutes, latitude, longitude, timezone_offset on public.profiles for each row execute function public.invalidate_agentic_rectification_results_on_profile_change(); create or replace function public.accept_agentic_rectification_candidate( p_user_id uuid, p_session_id uuid, p_result_id uuid, p_time time without time zone ) returns jsonb language plpgsql security definer set search_path = '' as $$ declare v_result public.agentic_rectification_results%rowtype; v_profile public.profiles%rowtype; v_status text; v_selection_kind text; begin if p_user_id is null or p_session_id is null or p_result_id is null or p_time is null or extract(second from p_time) is distinct from 0 then raise exception 'agentic_rectification_candidate_invalid_input' 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 session_id = p_session_id for update; if not found then raise exception 'agentic_rectification_candidate_not_found' 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.selection_allowed then raise exception 'agentic_rectification_candidate_selection_blocked' using errcode = 'P0001'; end if; if not exists ( select 1 from pg_catalog.jsonb_array_elements(v_result.candidates) candidate where candidate ->> 'time' = pg_catalog.to_char(p_time, 'HH24:MI') ) then raise exception 'agentic_rectification_candidate_time_not_allowed' using errcode = 'P0001'; end if; if v_result.selected_time is not null then if v_result.selected_time is distinct from p_time then raise exception 'agentic_rectification_candidate_already_selected' using errcode = 'P0001'; end if; select * into v_profile from public.profiles where id = p_user_id for update; if not found or v_profile.active_birth_time is distinct from v_result.selected_time or v_profile.birth_time is distinct from v_result.selected_time or v_profile.birth_time_status is distinct from ( case when v_result.selection_kind = 'engine_confirmed' then 'confirmed' else 'accepted' end ) then raise exception 'agentic_rectification_candidate_profile_changed' using errcode = 'P0001'; end if; return jsonb_build_object( 'success', true, 'saved_time', pg_catalog.to_char(v_result.selected_time, 'HH24:MI'), 'status', case when v_result.selection_kind = 'engine_confirmed' then 'confirmed' else 'accepted' end, 'result_id', v_result.id, 'idempotent', true ); end if; if exists ( select 1 from public.agentic_rectification_results newer where newer.user_id = p_user_id and newer.session_id = p_session_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 or v_profile.birth_date is distinct from v_result.baseline_birth_date or v_profile.reported_birth_time is distinct from v_result.baseline_reported_birth_time or v_profile.active_birth_time is distinct from v_result.baseline_active_birth_time or v_profile.birth_time_source is distinct from v_result.baseline_birth_time_source or v_profile.birth_time_period is distinct from v_result.baseline_birth_time_period or v_profile.uncertainty_before_minutes is distinct from v_result.baseline_uncertainty_before_minutes or v_profile.uncertainty_after_minutes is distinct from v_result.baseline_uncertainty_after_minutes or v_profile.latitude is distinct from v_result.baseline_latitude or v_profile.longitude is distinct from v_result.baseline_longitude or v_profile.timezone_offset is distinct from v_result.baseline_timezone_offset then raise exception 'agentic_rectification_candidate_profile_changed' using errcode = 'P0001'; end if; v_selection_kind := case when v_result.confirmation_allowed and v_result.representative_time is not distinct from p_time then 'engine_confirmed' else 'user_accepted' end; v_status := case when v_selection_kind = 'engine_confirmed' then 'confirmed' else 'accepted' end; update public.profiles set active_birth_time = p_time, birth_time = p_time, birth_time_status = v_status, 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_time = p_time, selection_kind = v_selection_kind, 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 id <> v_result.id and invalidated_at is null and selected_time is null; return jsonb_build_object( 'success', true, 'saved_time', pg_catalog.to_char(p_time, 'HH24:MI'), 'status', v_status, 'result_id', v_result.id, 'idempotent', false ); end; $$; revoke all on function public.accept_agentic_rectification_candidate(uuid, uuid, uuid, time without time zone) from public, anon, authenticated; grant execute on function public.accept_agentic_rectification_candidate(uuid, uuid, uuid, time without time zone) to service_role; commit;