-- Declared uncertainty: sub-window block_scan, widen RPC, date reliability. -- Exclusive span > 120 stays in block_scan; inclusive width cap for widen is 241. begin; do $migration$ begin if current_user <> 'schema_owner' then raise exception 'rectification_declared_uncertainty_requires_schema_owner' using errcode = '42501'; end if; end $migration$; alter table public.agentic_rectification_cases add column if not exists widen_declined_at_fingerprint text; comment on column public.agentic_rectification_cases.widen_declined_at_fingerprint is 'Evidence ledger fingerprint at which the user declined a widen-window card.'; alter table public.agentic_rectification_evidence add column if not exists date_reliability text; alter table public.agentic_rectification_evidence drop constraint if exists agentic_rectification_evidence_date_reliability_check; alter table public.agentic_rectification_evidence add constraint agentic_rectification_evidence_date_reliability_check check ( date_reliability is null or date_reliability in ('high', 'medium', 'low', 'uncertain', 'unreliable') ); comment on column public.agentic_rectification_evidence.date_reliability is 'Optional day-event reliability: high = checked record, medium = memory.'; create or replace function public.agentic_rectification_is_clock(p_clock text) returns boolean language sql immutable parallel safe set search_path = '' as $$ select p_clock is not null and p_clock ~ '^([01][0-9]|2[0-3]):[0-5][0-9]$'; $$; create or replace function public.agentic_rectification_clock_minutes(p_clock text) returns integer language sql immutable strict parallel safe set search_path = '' as $$ select split_part(p_clock, ':', 1)::integer * 60 + split_part(p_clock, ':', 2)::integer; $$; create or replace function public.agentic_rectification_clock_span_minutes( p_start text, p_end text ) returns integer language sql immutable strict parallel safe set search_path = '' as $$ select case when public.agentic_rectification_clock_minutes(p_end) >= public.agentic_rectification_clock_minutes(p_start) then public.agentic_rectification_clock_minutes(p_end) - public.agentic_rectification_clock_minutes(p_start) else 1440 - public.agentic_rectification_clock_minutes(p_start) + public.agentic_rectification_clock_minutes(p_end) end; $$; create or replace function public.agentic_rectification_clock_inclusive_width( p_start text, p_end text ) returns integer language sql immutable strict parallel safe set search_path = '' as $$ select public.agentic_rectification_clock_span_minutes(p_start, p_end) + 1; $$; create or replace function public.agentic_rectification_clock_contains( p_outer_start text, p_outer_end text, p_inner_start text, p_inner_end text ) returns boolean language plpgsql immutable strict parallel safe set search_path = '' as $$ declare v_outer_width integer; v_origin integer; v_inner_start integer; v_inner_end integer; v_outer_last integer; begin v_outer_width := public.agentic_rectification_clock_inclusive_width(p_outer_start, p_outer_end); if v_outer_width >= 1440 then return true; end if; v_origin := public.agentic_rectification_clock_minutes(p_outer_start); v_inner_start := ( public.agentic_rectification_clock_minutes(p_inner_start) - v_origin + 1440 ) % 1440; v_inner_end := ( public.agentic_rectification_clock_minutes(p_inner_end) - v_origin + 1440 ) % 1440; v_outer_last := v_outer_width - 1; if v_inner_start <= v_inner_end then return v_inner_start <= v_outer_last and v_inner_end <= v_outer_last; end if; return false; end; $$; revoke all on function public.agentic_rectification_is_clock(text) from public, anon, authenticated; revoke all on function public.agentic_rectification_clock_minutes(text) from public, anon, authenticated; revoke all on function public.agentic_rectification_clock_span_minutes(text, text) from public, anon, authenticated; revoke all on function public.agentic_rectification_clock_inclusive_width(text, text) from public, anon, authenticated; revoke all on function public.agentic_rectification_clock_contains(text, text, text, text) from public, anon, authenticated; grant execute on function public.agentic_rectification_is_clock(text) to service_role; grant execute on function public.agentic_rectification_clock_minutes(text) to service_role; grant execute on function public.agentic_rectification_clock_span_minutes(text, text) to service_role; grant execute on function public.agentic_rectification_clock_inclusive_width(text, text) to service_role; grant execute on function public.agentic_rectification_clock_contains(text, text, text, text) to service_role; create or replace function public.set_agentic_rectification_case_stage( p_user_id uuid, p_case_id uuid, p_stage text ) returns jsonb language plpgsql security definer set search_path = '' as $$ declare v_case public.agentic_rectification_cases%rowtype; begin if p_user_id is null or p_case_id is null or p_stage not in ('minute', 'block_scan') then raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001'; end if; 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; if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001'; end if; update public.agentic_rectification_cases set stage = p_stage, last_activity_at = pg_catalog.now() where id = v_case.id; return jsonb_build_object( 'case_id', v_case.id, 'stage', p_stage ); end; $$; create or replace function public.advance_agentic_rectification_case_from_block_scan( p_user_id uuid, p_case_id uuid, p_start_time text, p_end_time text ) returns jsonb language plpgsql security definer set search_path = '' as $$ declare v_case public.agentic_rectification_cases%rowtype; v_range jsonb; v_current_start text; v_current_end text; v_span integer; v_rounds integer; v_stage text; v_block_scan jsonb; begin if p_user_id is null or p_case_id is null or not public.agentic_rectification_is_clock(p_start_time) or not public.agentic_rectification_is_clock(p_end_time) or p_start_time = p_end_time then raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001'; end if; 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; if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001'; end if; if v_case.stage is distinct from 'block_scan' then raise exception 'agentic_rectification_not_block_scan' using errcode = 'P0001'; end if; v_current_start := coalesce(v_case.candidate_range->>'start_time', ''); v_current_end := coalesce(v_case.candidate_range->>'end_time', ''); if not public.agentic_rectification_is_clock(v_current_start) or not public.agentic_rectification_is_clock(v_current_end) then raise exception 'agentic_rectification_invalid_range' using errcode = 'P0001'; end if; if not public.agentic_rectification_clock_contains( v_current_start, v_current_end, p_start_time, p_end_time ) then raise exception 'agentic_rectification_invalid_block_window' using errcode = 'P0001'; end if; v_span := public.agentic_rectification_clock_span_minutes(p_start_time, p_end_time); begin v_rounds := coalesce((v_case.block_scan->>'rounds')::integer, 0); exception when invalid_text_representation then v_rounds := 0; end; if v_rounds < 0 then v_rounds := 0; end if; v_range := jsonb_build_object('start_time', p_start_time, 'end_time', p_end_time); if v_span > 120 and v_rounds < 3 then v_stage := 'block_scan'; v_block_scan := jsonb_build_object('rounds', v_rounds); else v_stage := 'minute'; v_block_scan := null; end if; update public.agentic_rectification_results set invalidated_at = pg_catalog.now() where case_id = v_case.id and invalidated_at is null; update public.agentic_rectification_cases set candidate_range = v_range, stage = v_stage, block_scan = v_block_scan, last_activity_at = pg_catalog.now() where id = v_case.id; return jsonb_build_object( 'case_id', v_case.id, 'stage', v_stage, 'candidate_range', v_range ); end; $$; create or replace function public.widen_agentic_rectification_case_window( p_user_id uuid, p_case_id uuid, p_start_time text, p_end_time text ) returns jsonb language plpgsql security definer set search_path = '' as $$ declare v_case public.agentic_rectification_cases%rowtype; v_range jsonb; v_current_start text; v_current_end text; v_old_width integer; v_new_width integer; begin if p_user_id is null or p_case_id is null or not public.agentic_rectification_is_clock(p_start_time) or not public.agentic_rectification_is_clock(p_end_time) or p_start_time = p_end_time then raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001'; end if; 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; if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001'; end if; if v_case.accepted_time is not null then raise exception 'agentic_rectification_already_adopted' using errcode = 'P0001'; end if; if coalesce(v_case.stage, 'minute') is distinct from 'minute' then raise exception 'agentic_rectification_not_minute' using errcode = 'P0001'; end if; v_current_start := coalesce(v_case.candidate_range->>'start_time', ''); v_current_end := coalesce(v_case.candidate_range->>'end_time', ''); if not public.agentic_rectification_is_clock(v_current_start) or not public.agentic_rectification_is_clock(v_current_end) then raise exception 'agentic_rectification_invalid_range' using errcode = 'P0001'; end if; if not public.agentic_rectification_clock_contains( p_start_time, p_end_time, v_current_start, v_current_end ) then raise exception 'agentic_rectification_invalid_widen_window' using errcode = 'P0001'; end if; v_old_width := public.agentic_rectification_clock_inclusive_width(v_current_start, v_current_end); v_new_width := public.agentic_rectification_clock_inclusive_width(p_start_time, p_end_time); if v_new_width <= v_old_width or v_new_width > 241 then raise exception 'agentic_rectification_invalid_widen_window' using errcode = 'P0001'; end if; v_range := jsonb_build_object('start_time', p_start_time, 'end_time', p_end_time); update public.agentic_rectification_results set invalidated_at = pg_catalog.now() where case_id = v_case.id and invalidated_at is null; update public.agentic_rectification_cases set candidate_range = v_range, last_activity_at = pg_catalog.now() where id = v_case.id; return jsonb_build_object( 'case_id', v_case.id, 'stage', 'minute', 'candidate_range', v_range ); end; $$; create or replace function public.set_agentic_rectification_widen_declined( p_user_id uuid, p_case_id uuid, p_fingerprint text ) returns jsonb language plpgsql security definer set search_path = '' as $$ declare v_case public.agentic_rectification_cases%rowtype; begin if p_user_id is null or p_case_id is null or p_fingerprint is null or btrim(p_fingerprint) = '' then raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001'; end if; 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; if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001'; end if; update public.agentic_rectification_cases set widen_declined_at_fingerprint = btrim(p_fingerprint), last_activity_at = pg_catalog.now() where id = v_case.id; return jsonb_build_object( 'case_id', v_case.id, 'widen_declined_at_fingerprint', btrim(p_fingerprint) ); end; $$; create or replace function public.set_agentic_rectification_evidence_date_reliability( p_user_id uuid, p_case_id uuid, p_evidence_id uuid, p_date_reliability text ) returns jsonb language plpgsql security definer set search_path = '' as $$ declare v_case public.agentic_rectification_cases%rowtype; v_evidence public.agentic_rectification_evidence%rowtype; begin if p_user_id is null or p_case_id is null or p_evidence_id is null or p_date_reliability not in ('high', 'medium') then raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001'; end if; 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; if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001'; end if; select * into v_evidence from public.agentic_rectification_evidence where id = p_evidence_id and case_id = v_case.id for update; if not found then raise exception 'agentic_rectification_evidence_not_found' using errcode = 'P0001'; end if; update public.agentic_rectification_evidence set date_reliability = p_date_reliability where id = v_evidence.id; return jsonb_build_object( 'evidence_id', v_evidence.id, 'date_reliability', p_date_reliability ); end; $$; revoke all on function public.widen_agentic_rectification_case_window(uuid, uuid, text, text) from public, anon, authenticated; grant execute on function public.widen_agentic_rectification_case_window(uuid, uuid, text, text) to service_role; revoke all on function public.set_agentic_rectification_widen_declined(uuid, uuid, text) from public, anon, authenticated; grant execute on function public.set_agentic_rectification_widen_declined(uuid, uuid, text) to service_role; revoke all on function public.set_agentic_rectification_evidence_date_reliability(uuid, uuid, uuid, text) from public, anon, authenticated; grant execute on function public.set_agentic_rectification_evidence_date_reliability(uuid, uuid, uuid, text) to service_role; create or replace function public.get_agentic_rectification_case( p_user_id uuid, p_case_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_evidence_count bigint; v_turn_count bigint; begin if p_user_id is null or p_case_id is null then raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001'; end if; select * into v_case from public.agentic_rectification_cases where id = p_case_id and user_id = p_user_id; if not found then raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001'; end if; select count(*) into v_evidence_count from public.agentic_rectification_evidence where case_id = v_case.id; select count(*) into v_turn_count from public.agentic_rectification_turns where case_id = v_case.id; select * into v_result from public.agentic_rectification_results where case_id = v_case.id and invalidated_at is null order by created_at desc limit 1; return jsonb_build_object( 'case_id', v_case.id, 'session_id', v_case.session_id, 'status', v_case.status, 'skill_name', v_case.skill_name, 'skill_version', v_case.skill_version, 'candidate_range', v_case.candidate_range, 'stage', coalesce(v_case.stage, 'minute'), 'block_scan', v_case.block_scan, 'widen_declined_at_fingerprint', v_case.widen_declined_at_fingerprint, 'reported_birth_time', v_case.baseline_birth_snapshot->>'reported_birth_time', 'birth_time_source', v_case.baseline_birth_snapshot->>'birth_time_source', 'accepted_time', v_case.accepted_time, 'confirmed_time', v_case.confirmed_time, 'created_at', v_case.created_at, 'last_activity_at', v_case.last_activity_at, 'completed_at', v_case.completed_at, 'closed_reason', v_case.closed_reason, 'evidence_count', v_evidence_count, 'turn_count', v_turn_count, 'latest_result', case when v_result.id is null then null else jsonb_build_object( 'result_id', v_result.id, 'candidates', v_result.candidates, 'overall_confidence', v_result.overall_confidence, 'display_allowed', v_result.display_allowed, 'selection_allowed', v_result.selection_allowed, 'confirmation_allowed', v_result.confirmation_allowed, 'representative_time', v_result.representative_time, 'selected_candidate_id', v_result.selected_candidate_id, 'selected_time', v_result.selected_time, 'selection_kind', v_result.selection_kind, 'evidence_ledger_fingerprint', v_result.evidence_ledger_fingerprint, 'candidate_range_fingerprint', v_result.candidate_range_fingerprint, 'skill_version', v_result.skill_version, 'algorithm_version', v_result.algorithm_version, 'event_contract_version', v_result.event_contract_version, 'decision_policy_version', v_result.decision_policy_version, 'decision_receipt', public.compose_agentic_rectification_decision_receipt(v_case.id, v_result.id, v_result.decision_receipt), 'execution_ledger', v_result.execution_ledger, 'created_at', v_result.created_at, 'invalidated_at', v_result.invalidated_at ) end ); end; $$; revoke all on function public.get_agentic_rectification_case(uuid, uuid) from public, anon, authenticated; grant execute on function public.get_agentic_rectification_case(uuid, uuid) to service_role; create or replace function public.get_agentic_rectification_case_dossier( p_user_id uuid, p_case_id uuid ) returns jsonb language plpgsql security definer set search_path = '' as $$ declare v_case public.agentic_rectification_cases%rowtype; v_turns jsonb; v_evidence jsonb; v_summary public.agentic_rectification_case_conversation_summaries%rowtype; v_result public.agentic_rectification_results%rowtype; v_evidence_count bigint; v_turn_count bigint; begin if p_user_id is null or p_case_id is null then raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001'; end if; select * into v_case from public.agentic_rectification_cases where id = p_case_id and user_id = p_user_id; if not found then raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001'; end if; select coalesce(jsonb_agg(jsonb_build_object( 'id', recent.id, 'role', message.role, 'text', message.text, 'status', recent.status, 'created_at', recent.created_at, 'completed_at', recent.completed_at ) order by recent.created_at, recent.id, message.ordinal), '[]'::jsonb) into v_turns from ( select t.* from public.agentic_rectification_turns t where t.case_id = v_case.id order by t.created_at desc, t.id desc limit 50 ) recent cross join lateral ( values (1, 'user'::text, recent.user_message), (2, 'assistant'::text, recent.assistant_message) ) as message(ordinal, role, text) where message.text is not null; select coalesce(jsonb_agg(jsonb_build_object( 'id', e.id, 'source_turn_id', e.source_turn_id, 'subject', e.subject, 'event_kind', e.event_kind, 'domain', e.domain, 'occurred_from', e.occurred_from, 'occurred_to', e.occurred_to, 'date_precision', e.date_precision, 'date_reliability', e.date_reliability, 'summary', e.summary, 'status', e.status, 'supersedes_evidence_id', e.supersedes_evidence_id, 'created_at', e.created_at ) order by e.created_at, e.id), '[]'::jsonb) into v_evidence from public.agentic_rectification_evidence e where e.case_id = v_case.id; select count(*) into v_evidence_count from public.agentic_rectification_evidence where case_id = v_case.id; select count(*) into v_turn_count from public.agentic_rectification_turns where case_id = v_case.id; select * into v_summary from public.agentic_rectification_case_conversation_summaries where case_id = v_case.id; if not found then perform public.refresh_agentic_rectification_case_conversation_summary(v_case.id); select * into v_summary from public.agentic_rectification_case_conversation_summaries where case_id = v_case.id; end if; select * into v_result from public.agentic_rectification_results where case_id = v_case.id and invalidated_at is null order by created_at desc, id desc limit 1; return jsonb_build_object( 'case', jsonb_build_object( 'case_id', v_case.id, 'session_id', v_case.session_id, 'status', v_case.status, 'skill_name', v_case.skill_name, 'skill_version', v_case.skill_version, 'candidate_range', v_case.candidate_range, 'stage', coalesce(v_case.stage, 'minute'), 'block_scan', v_case.block_scan, 'widen_declined_at_fingerprint', v_case.widen_declined_at_fingerprint, 'reported_birth_time', v_case.baseline_birth_snapshot->>'reported_birth_time', 'birth_time_source', v_case.baseline_birth_snapshot->>'birth_time_source', 'accepted_time', v_case.accepted_time, 'confirmed_time', v_case.confirmed_time, 'completed_at', v_case.completed_at, 'closed_reason', v_case.closed_reason, 'last_activity_at', v_case.last_activity_at, 'evidence_count', v_evidence_count, 'turn_count', v_turn_count ), 'turns', v_turns, 'evidence', v_evidence, 'conversation_summary', jsonb_build_object( 'confirmed_evidence_summary', v_summary.confirmed_evidence_summary, 'pending_revisions', v_summary.pending_revisions, 'active_focus', v_summary.active_focus, 'declined_skipped_topics', v_summary.declined_skipped_topics, 'candidate_divergence_summary', v_summary.candidate_divergence_summary, 'missing_evidence_categories', v_summary.missing_evidence_categories, 'last_result_policy', v_summary.last_result_policy, 'summary_version', v_summary.summary_version, 'updated_at', v_summary.updated_at ), 'latest_result', case when v_result.id is null then null else jsonb_build_object( 'result_id', v_result.id, 'candidates', v_result.candidates, 'overall_confidence', v_result.overall_confidence, 'display_allowed', v_result.display_allowed, 'selection_allowed', v_result.selection_allowed, 'confirmation_allowed', v_result.confirmation_allowed, 'representative_time', v_result.representative_time, 'selected_candidate_id', v_result.selected_candidate_id, 'selected_time', v_result.selected_time, 'selection_kind', v_result.selection_kind, 'evidence_ledger_fingerprint', v_result.evidence_ledger_fingerprint, 'candidate_range_fingerprint', v_result.candidate_range_fingerprint, 'skill_version', v_result.skill_version, 'algorithm_version', v_result.algorithm_version, 'event_contract_version', v_result.event_contract_version, 'decision_policy_version', v_result.decision_policy_version, 'decision_receipt', public.compose_agentic_rectification_decision_receipt(v_case.id, v_result.id, v_result.decision_receipt), 'execution_ledger', v_result.execution_ledger, 'created_at', v_result.created_at, 'invalidated_at', v_result.invalidated_at ) end ); end; $$; revoke all on function public.get_agentic_rectification_case_dossier(uuid, uuid) from public, anon, authenticated; grant execute on function public.get_agentic_rectification_case_dossier(uuid, uuid) to service_role; commit;