begin; -- Dateless occupation_note (and other background notes) can be confirmed -- without a calendar date. Career dated events still do not cover occupation. create or replace function public.agentic_rectification_allows_dateless_confirm(p_kind text) returns boolean language sql immutable as $$ select p_kind in ('occupation_note', 'appearance_note', 'birthmark_or_scar') $$; revoke all on function public.agentic_rectification_allows_dateless_confirm(text) from public, anon, authenticated; grant execute on function public.agentic_rectification_allows_dateless_confirm(text) to service_role; create or replace function public.record_agentic_rectification_evidence_batch( p_user_id uuid, p_case_id uuid, p_source_turn_id uuid, p_focus_id uuid, p_items jsonb ) returns jsonb language plpgsql security definer set search_path = '' as $$ declare v_case public.agentic_rectification_cases%rowtype; v_turn public.agentic_rectification_turns%rowtype; v_focus public.agentic_rectification_conversation_focuses%rowtype; v_focus_replay boolean := false; v_focus_match_id uuid; v_focus_match_count integer := 0; v_focus_resolution text := 'not_requested'; v_item jsonb; v_index bigint; v_item_key text; v_quote text; v_subject text; v_kind text; v_domain text; v_precision text; v_summary text; v_from date; v_to date; v_status text; v_outcome text; v_error text; v_clarification jsonb; v_existing public.agentic_rectification_evidence%rowtype; v_evidence_id uuid; v_idempotent boolean; v_results jsonb := '[]'::jsonb; v_accepted integer := 0; v_needs integer := 0; v_rejected integer := 0; begin if p_user_id is null or p_case_id is null or p_source_turn_id is null or p_items is null or jsonb_typeof(p_items) <> 'array' or jsonb_array_length(p_items) = 0 or jsonb_array_length(p_items) > 12 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_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 then raise exception 'agentic_rectification_quote_not_grounded' using errcode = 'P0001'; end if; if p_focus_id is not null then select * into v_focus from public.agentic_rectification_conversation_focuses where id = p_focus_id and case_id = p_case_id for update; if not found or v_focus.status not in ('active', 'resolved') then raise exception 'agentic_rectification_focus_not_active' using errcode = 'P0001'; end if; v_focus_replay := v_focus.status = 'resolved'; if not v_focus_replay and v_focus.target_evidence_id is not null then raise exception 'agentic_rectification_focus_target_mismatch' using errcode = 'P0001'; end if; v_focus_resolution := case when v_focus_replay then 'idempotent' else 'active' end; if v_focus_replay and exists ( select 1 from jsonb_array_elements(p_items) item where length(btrim(coalesce(item->>'idempotency_key', ''))) = 0 or not exists ( select 1 from public.agentic_rectification_evidence e where e.case_id = p_case_id and e.idempotency_key = btrim(item->>'idempotency_key') ) ) then raise exception 'agentic_rectification_focus_not_active' using errcode = 'P0001'; end if; end if; for v_item, v_index in select value, ordinality from jsonb_array_elements(p_items) with ordinality loop v_item_key := btrim(coalesce(v_item->>'idempotency_key', '')); v_quote := btrim(coalesce(v_item->>'quote', '')); v_subject := coalesce(v_item->>'subject', ''); v_kind := coalesce(v_item->>'event_kind', ''); v_domain := coalesce(v_item->>'domain', ''); v_precision := coalesce(v_item->>'date_precision', ''); v_summary := btrim(coalesce(v_item->>'summary', '')); v_from := null; v_to := null; v_error := null; v_clarification := '[]'::jsonb; v_evidence_id := null; v_idempotent := false; begin if nullif(v_item->>'occurred_from', '') is not null then v_from := (v_item->>'occurred_from')::date; end if; if nullif(v_item->>'occurred_to', '') is not null then v_to := (v_item->>'occurred_to')::date; end if; exception when others then v_error := 'invalid_date'; end; if v_error is null and ( length(v_item_key) = 0 or length(v_item_key) > 160 or length(v_quote) = 0 or length(v_summary) = 0 or v_subject not in ('self', 'family', 'other') or not (v_kind = any (public.agentic_rectification_evidence_kinds())) or not (v_domain = any (public.agentic_rectification_evidence_domains())) or not (v_precision = any (public.agentic_rectification_date_precisions())) ) then v_error := 'invalid_item'; end if; if v_error is null and position( public.agentic_rectification_normalize_quote(v_quote) in public.agentic_rectification_normalize_quote(v_turn.user_message) ) = 0 then v_error := 'quote_not_grounded'; end if; if v_error is null and v_precision = 'range' and (v_from is null or v_to is null or v_from > v_to) then v_error := 'invalid_range'; end if; if v_error is not null then v_outcome := 'rejected'; v_status := 'rejected'; v_rejected := v_rejected + 1; else select * into v_existing from public.agentic_rectification_evidence where case_id = p_case_id and idempotency_key = v_item_key; if found then if v_existing.source_turn_id is distinct from p_source_turn_id or v_existing.user_quote is distinct from v_quote or v_existing.subject is distinct from v_subject or v_existing.event_kind is distinct from v_kind or v_existing.domain is distinct from v_domain or v_existing.occurred_from is distinct from v_from or v_existing.occurred_to is distinct from v_to or v_existing.date_precision is distinct from v_precision or v_existing.summary is distinct from v_summary then v_outcome := 'rejected'; v_status := 'rejected'; v_error := 'idempotency_conflict'; v_rejected := v_rejected + 1; else v_evidence_id := v_existing.id; v_idempotent := true; if v_existing.status = 'confirmed' then v_outcome := 'accepted'; v_status := 'confirmed'; v_accepted := v_accepted + 1; elsif public.agentic_rectification_allows_dateless_confirm(v_kind) and (v_precision = 'unknown' or v_from is null) then update public.agentic_rectification_evidence set status = 'confirmed', confirmed_at = coalesce(confirmed_at, pg_catalog.now()), updated_at = pg_catalog.now() where id = v_existing.id and status is distinct from 'confirmed'; v_outcome := 'accepted'; v_status := 'confirmed'; v_accepted := v_accepted + 1; else v_outcome := 'needs_clarification'; v_status := v_existing.status; v_clarification := case when v_existing.date_precision = 'unknown' then '["date"]'::jsonb else '[]'::jsonb end; v_needs := v_needs + 1; end if; end if; else if (v_precision = 'unknown' or v_from is null) and not public.agentic_rectification_allows_dateless_confirm(v_kind) then v_outcome := 'needs_clarification'; v_status := 'draft'; v_clarification := '["date"]'::jsonb; v_needs := v_needs + 1; else v_outcome := 'accepted'; v_status := 'confirmed'; v_accepted := v_accepted + 1; end if; insert into public.agentic_rectification_evidence ( case_id, source_turn_id, user_quote, subject, event_kind, domain, occurred_from, occurred_to, date_precision, summary, status, confirmed_at, idempotency_key ) values ( p_case_id, p_source_turn_id, v_quote, v_subject, v_kind, v_domain, v_from, v_to, v_precision, v_summary, v_status, case when v_status = 'confirmed' then pg_catalog.now() else null end, v_item_key ) returning id into v_evidence_id; end if; end if; if p_focus_id is not null and v_outcome = 'accepted' and v_evidence_id is not null and (v_focus.target_domain is null or v_domain = v_focus.target_domain) and (v_focus.target_kind is null or v_kind = v_focus.target_kind) then v_focus_match_count := v_focus_match_count + 1; if v_focus_match_id is null then v_focus_match_id := v_evidence_id; end if; end if; v_results := v_results || jsonb_build_array(jsonb_build_object( 'index', v_index - 1, 'idempotency_key', nullif(v_item_key, ''), 'outcome', v_outcome, 'evidence_id', v_evidence_id, 'status', v_status, 'idempotent', v_idempotent, 'clarification_fields', v_clarification, 'error_code', v_error )); end loop; update public.agentic_rectification_cases set status = case when status = 'draft' and v_accepted > 0 then 'collecting_evidence' else status end, last_activity_at = pg_catalog.now(), updated_at = pg_catalog.now() where id = p_case_id; if p_focus_id is not null then if v_focus_replay then if v_focus_match_count <> 1 or v_focus_match_id is distinct from v_focus.target_evidence_id then raise exception 'agentic_rectification_focus_idempotency_conflict' using errcode = 'P0001'; end if; v_focus_resolution := 'idempotent'; elsif v_focus_match_count = 1 then perform public.resolve_agentic_rectification_conversation_focus( p_user_id, p_case_id, p_focus_id, 'resolved', v_focus_match_id ); v_focus_resolution := 'resolved'; elsif v_focus_match_count > 1 then v_focus_resolution := 'ambiguous'; else v_focus_resolution := 'unmatched'; end if; end if; return jsonb_build_object( 'items', v_results, 'accepted_count', v_accepted, 'needs_clarification_count', v_needs, 'rejected_count', v_rejected, 'focus_id', p_focus_id, 'focus_evidence_id', v_focus_match_id, 'focus_resolution', v_focus_resolution ); end; $$; revoke all on function public.record_agentic_rectification_evidence_batch( uuid, uuid, uuid, uuid, jsonb ) from public, anon, authenticated; grant execute on function public.record_agentic_rectification_evidence_batch( uuid, uuid, uuid, uuid, jsonb ) to service_role; update public.agentic_rectification_evidence set status = 'confirmed', confirmed_at = coalesce(confirmed_at, pg_catalog.now()), updated_at = pg_catalog.now() where event_kind in ('occupation_note', 'appearance_note', 'birthmark_or_scar') and status in ('draft', 'pending_confirmation') and (date_precision = 'unknown' or occurred_from is null); commit;